package midcode;

import optimizer.BasicBlock;
import optimizer.RegPool;
import symbol.Function;
import symbol.Symbol;
import symbol.SymbolTable;
import optimizer.Optimizer;
import java.util.ArrayList;

public class MidCode {
    private final MidcodeGenerator mg;
    private final SymbolTable table;
    private final String funcName;
    private OPType opType;
    private String val1;
    private String val2;
    private String dst;
    private int index;
    private RegPool regPool;
    private StringBuilder target;

    private boolean isEntry = false;
    private BasicBlock basicBlock;

    public MidCode(OPType opType, String val1, String val2, String dst) {
        mg = MidcodeGenerator.getInstance();
        table = mg.getSymbolTable();
        funcName = mg.getFuncName();
        this.opType = opType;
        this.val1 = val1;
        this.val2 = val2;
        this.dst = dst;
    }

    public MidCode(OPType opType, String val1, String val2, String dst, String funcName) {
        mg = MidcodeGenerator.getInstance();
        table = mg.getSymbolTable();
        this.funcName = funcName;
        this.opType = opType;
        this.val1 = val1;
        this.val2 = val2;
        this.dst = dst;
    }

    public String getFuncName() {
        return funcName;
    }

    public OPType getOpType() {
        return opType;
    }

    public String getVal1() {
        return val1;
    }

    public String getVal2() {
        return val2;
    }

    public String getDst() {
        return dst;
    }

    public void setVal1(String val1) {
        this.val1 = val1;
    }

    public void setVal2(String val2) {
        this.val2 = val2;
    }

    public void setDst(String dst) {
        this.dst = dst;
    }

    public boolean isEntry() {
        return isEntry;
    }

    public void setEntry(boolean entryPoint) {
        isEntry = entryPoint;
    }

    public void setBasicBlock(BasicBlock basicBlock) {
        this.basicBlock = basicBlock;
    }

    public String genTargetCode(int index) {
        this.index = index;
        this.regPool = mg.getRegPool();
        target = new StringBuilder();
        target.append("###### ").append(this).append(" ######\n");
        switch (opType) {
            case GLOBAL_VAL:
                genGlobalValues();
                break;
            case ASSIGN:
                genAssign();
                break;
            case LW_ARRAY:
                genLWArray();
                break;
            case SW_ARRAY:
                genSWArray();
                break;
            case GETINT:
                genGetInt();
                break;
            case PRINT_INT:
                genPrintInt();
                break;
            case PRINT_STR:
                genPrintStr();
                break;
            case LABEL_GEN:
                genLabel();
                break;
            case GOTO:
                genGoto();
                break;
            case FUNC_DECL:
                mg.startFuncDef(dst);
                genFuncDecl();
                break;
            case PUSH_PARAM:
                genPushParam();
                break;
            case LW_ADDRESS:
                genLWAddress();
                break;
            case RET_INT:
                genRetInt();
                break;
            case RET_VOID:
                genRetVoid();
                break;
            case EXIT:
                genExit();
                break;
            case PRE_CALL:
                genPreCall();
                break;
            case CALL:
                genCall();
                break;
            case FIN_CALL:
                genFinCall();
                break;
            case GET_RET:
                genGetRet();
                break;
            case MULT:
                genMULT();
                break;
            case ADD:
                genADD();
                break;
            case SUB:
                genSUB();
                break;
            case DIV:
                genDIV();
                break;
            case MOD:
                genMOD();
                break;
            case NOT:
                genNOT();
                break;
            case SLT:
                genSLT();
                break;
            case SLE:
                genSLE();
                break;
            case SGT:
                genSGT();
                break;
            case SGE:
                genSGE();
                break;
            case SEQ:
                genSEQ();
                break;
            case SNE:
                genSNE();
                break;
            case BEQ:
                genBEQ();
                break;
            case BNE:
                genBNE();
                break;
        }
        return target.toString();
    }

    private void genGlobalValues() {
        target.append(dst).append(":.word ");
        Symbol symbol = table.searchGlobalBlock(dst);
        ArrayList<Integer> flatArray = symbol.getFlatArray();
        target.append(flatArray.get(0).toString());
        for (int i = 1; i < flatArray.size(); i++) {
            target.append(",").append(flatArray.get(i).toString());
        }
        target.append("\n");
    }

    private void genAssign() {
        String reg3 = load(dst, "$t0");
        store(val1, reg3);
    }

    private void genLWArray() {
        Symbol symbol = table.searchGlobalBlock(val1);
        if (symbol != null) {  // global value
            if (mg.isNumber(val2)) {
                int offset_gp = symbol.getOffset_gp() + Integer.parseInt(val2) * 4;
                target.append(String.format("addiu $t0, $gp, %d\n", offset_gp));
            } else {
                int offset_gp = symbol.getOffset_gp();
                String reg2 = load(val2, "$t0");
                target.append(String.format("sll $t0, %s, 2\n", reg2));
                target.append(String.format("addiu $t0, $t0, %d\n", offset_gp));
                target.append("addu $t0, $t0, $gp\n");
            }
        } else {
            if (table.searchFuncParam(funcName, val1) != null) {  // func parameter
                if (mg.isNumber(val2)) {
                    String reg1 = load(val1, "$t0");
                    target.append(String.format("addiu $t0, %s, %d\n", reg1, Integer.parseInt(val2) * 4));
                } else {
                    String reg1 = load(val1, "$t0");
                    String reg2 = load(val2, "$t1");
                    target.append(String.format("sll $t1, %s, 2\n", reg2));
                    target.append(String.format("addu $t0, %s $t1\n", reg1));
                }
            } else {     // local value
                symbol = table.searchFuncBlock(funcName, val1);
                assert symbol != null;
                if (mg.isNumber(val2)) {
                    int offset_sp = symbol.getOffset_sp() + Integer.parseInt(val2) * 4;
                    target.append(String.format("addiu $t0, $sp, %d\n", offset_sp));
                } else {
                    int offset_sp = symbol.getOffset_sp();
                    String reg2 = load(val2, "$t0");
                    target.append(String.format("sll $t0, %s, 2\n", reg2));
                    target.append(String.format("addiu $t0, $t0, %d\n", offset_sp));
                    target.append("addu $t0, $t0, $sp\n");
                }
            }
        }
        String reg3 = regPool.allocTReg(dst, index);
        if (reg3.equals("$t0")) {
            target.append("lw $t0, 0($t0)\n");
            store(dst, "$t0");
        } else {
            target.append(String.format("lw %s, 0($t0)\n", reg3));
        }
    }

    private void genSWArray() {
        // t0存地址
        Symbol symbol = table.searchGlobalBlock(val1);
        if (symbol != null) {  // global value
            int offset_gp = symbol.getOffset_gp();
            String reg2 = load(val2, "$t0");
            target.append(String.format("sll $t0, %s, 2\n", reg2));
            target.append(String.format("addiu $t0, $t0, %d\n", offset_gp));
            target.append("addu $t0, $t0, $gp\n");
        } else {
            if (table.searchFuncParam(funcName, val1) != null) {  // func parameter
                String reg1 = load(val1, "$t0");
                if (mg.isNumber(val2)) {
                    target.append(String.format("addiu $t0, %s, %d\n", reg1, Integer.parseInt(val2) * 4));
                } else {
                    String reg2 = load(val2, "$t2");
                    target.append(String.format("sll $t2, %s, 2\n", reg2));
                    target.append(String.format("addu $t0, %s, $t2\n", reg1));
                }
            } else {  // local value
                symbol = table.searchFuncBlock(funcName, val1);
                assert symbol != null;
                if (mg.isNumber(val2)) {
                    int offset_sp = symbol.getOffset_sp() + Integer.parseInt(val2) * 4;
                    target.append(String.format("addiu $t0, $sp, %d\n", offset_sp));
                } else {
                    int offset_sp = symbol.getOffset_sp();
                    String reg2 = load(val2, "$t0");
                    target.append(String.format("sll $t0, %s, 2\n", reg2));
                    target.append(String.format("addiu $t0, $t0, %d\n", offset_sp));
                    target.append("addu $t0, $t0, $sp\n");
                }
            }
        }
        // t1存值
        if (mg.isNumber(dst)) {
            target.append(String.format("li $t1, %d\n", Integer.parseInt(dst)));
            target.append("sw $t1, 0($t0)\n");
        } else {
            String reg3 = load(dst, "$t1");
            target.append(String.format("sw %s, 0($t0)\n", reg3));
        }
    }

    private void genGetInt() {
        target.append("li $v0, 5\n");
        target.append("syscall\n");
        store(dst, "$v0");
    }

    private void genPrintInt() {
        String reg3 = load(dst, "$a0");
        if (!reg3.equals("$a0")) {
            target.append(String.format("addiu $a0, %s, 0\n", reg3));
        }
        target.append("li $v0, 1\n");
        target.append("syscall\n");
    }

    private void genPrintStr() {
        target.append(String.format("la $a0, %s\n", dst));
        target.append("li $v0, 4\n");
        target.append("syscall\n");
    }

    private void genLabel() {
        target.append(dst).append(":\n");
    }

    private void genGoto() {
        target.append(String.format("j %s\n", dst));
    }

    private void genFuncDecl() {
        target.append(dst).append(":\n");
    }

    private void genLWAddress() {
        Symbol symbol;
        if (val2 == null) {  // 实参为一维数组或二维数组
            if ((symbol = table.searchGlobalBlock(val1)) != null) {
                int offset_gp = symbol.getOffset_gp();
                target.append(String.format("addiu $t0, $gp, %d\n", offset_gp));
            } else {
                if (table.searchFuncParam(funcName, val1) != null) { // 函数参数
                    load(val1, "$t0");
                } else {
                    symbol = table.searchFuncBlock(funcName, val1);
                    assert symbol != null;
                    int offset_sp = symbol.getOffset_sp();
                    target.append(String.format("addiu $t0, $sp, %d\n", offset_sp));
                }
            }
        } else {  // 变量为二维数组，传参为一维数组
            if ((symbol = table.searchGlobalBlock(val1)) != null) {
                if (mg.isNumber(val2)) {
                    int offset_gp = symbol.getOffset_gp() + Integer.parseInt(val2) * symbol.getLen_2() * 4;
                    target.append(String.format("addiu $t0, $gp, %d\n", offset_gp));
                } else {
                    int offset_gp = symbol.getOffset_gp();
                    String reg2 = load(val2, "$t0");
                    target.append(String.format("li $t1, %d\n", symbol.getLen_2() * 4));
                    target.append(String.format("mult %s, $t1\n", reg2));
                    target.append("mflo $t0\n");
                    target.append(String.format("addiu $t0, $t0, %d\n", offset_gp));
                    target.append("addu $t0, $t0, $gp\n");
                }
            } else {
                if ((symbol = table.searchFuncParam(funcName, val1)) != null) {
                    load(val1, "$t0");
                    if (mg.isNumber(val2)) {
                        target.append(String.format("addiu $t0, $t0, %d\n", Integer.parseInt(val2) * symbol.getLen_2() * 4));
                    } else {
                        String reg2 = load(val2, "$t1");
                        target.append(String.format("li $t2, %d\n", symbol.getLen_2() * 4));
                        target.append(String.format("mult %s, $t2\n", reg2));
                        target.append("mflo $t1\n");
                        target.append("addu $t0, $t0, $t1\n");
                    }
                } else if ((symbol = table.searchFuncBlock(funcName, val1)) != null) {
                    if (mg.isNumber(val2)) {
                        int offset_sp = symbol.getOffset_sp() + Integer.parseInt(val2) * symbol.getLen_2() * 4;
                        target.append(String.format("addiu $t0, $sp, %d\n", offset_sp));
                    } else {
                        int offset_sp = symbol.getOffset_sp();
                        String reg2 = load(val2, "$t0");
                        target.append(String.format("li $t1, %d\n", symbol.getLen_2() * 4));
                        target.append(String.format("mult %s, $t1\n", reg2));
                        target.append("mflo $t0\n");
                        target.append(String.format("addiu $t0, $t0, %d\n", offset_sp));
                        target.append("addu $t0, $t0, $sp\n");
                    }
                }
            }
        }
        store(dst, "$t0");
    }

    private void genPushParam() {
        Function function = table.getFunction(dst);
        int size = function.getFuncSize();
        int param_offset = size - (Integer.parseInt(val2) + 1) * 4;
        String reg3 = "$t0";
        if (mg.isNumber(val1)) {
            target.append(String.format("li %s, %s\n", reg3, val1));
        } else {
            Symbol symbol = table.searchGlobalBlock(val1);
            if (symbol != null) { // global value
                target.append(String.format("lw %s, %d($gp)\n", reg3, symbol.getOffset_gp()));
            } else {  // local value
                reg3 = regPool.getTReg(val1);
                if (reg3.equals("$t0")) {
                    symbol = table.searchFuncBlock(funcName, val1);
                    target.append(String.format("lw %s, %d($sp)\n", reg3, symbol.getOffset_sp()));
                }
            }
        }
        target.append(String.format("sw %s, %d($sp)\n", reg3, param_offset));
    }

    private void genRetInt() {
        String reg3 = load(dst, "$v0");
        if (!reg3.equals("$v0")) {
            target.append(String.format("addiu $v0, %s, 0\n", reg3));
        }
        target.append("jr $ra\n");
    }

    private void genRetVoid() {
        target.append("jr $ra\n");
    }

    private void genExit() {
        target.append("li $v0, 10\n");
        target.append("syscall\n");
    }

    private void genPreCall() {
        int size = table.getFunction(dst).getFuncSize();
        target.append("sw $ra, 0($sp)\n");
        target.append(String.format("addiu $sp, $sp, -%d\n", size));
        mg.addStackSize(size);
    }

    private void genCall() {
        ArrayList<String> list = regPool.writeBackTReg(index);
        for (int i = 0; i < list.size(); i++) {
            store(list.get(i++), list.get(i));
        }
        regPool.clearTReg();
        target.append(String.format("jal %s\n", dst));
    }

    private void genFinCall() {
        int size = table.getFunction(dst).getFuncSize();
        target.append(String.format("addiu $sp, $sp, %d\n", size));
        mg.subStackSize(size);
        target.append("lw $ra, 0($sp)\n");
        regPool.clearTReg();
    }

    private void genGetRet() {
        int offset = table.searchFuncBlock(funcName, dst).getOffset_sp();
        String reg3 = regPool.allocTReg(dst, index);
        if (reg3.equals("$t0")) {
            target.append(String.format("sw $v0, %d($sp)\n", offset));
        } else {
            target.append(String.format("addiu %s, $v0, 0\n", reg3));
        }
    }

    private void genADD() {
        String reg1 = load(val1, "$t0");
        String reg2 = load(val2, "$t1");
        String reg3 = regPool.allocTReg(dst, index);
        target.append(String.format("addu %s, %s, %s\n", reg3, reg1, reg2));
        if (reg3.equals("$t0")) {
            store(dst, "$t0");
        }
    }

    private void genSUB() {
        String reg1 = load(val1, "$t0");
        String reg2 = load(val2, "$t1");
        String reg3 = regPool.allocTReg(dst, index);
        target.append(String.format("subu %s, %s, %s\n", reg3, reg1, reg2));
        if (reg3.equals("$t0")) {
            store(dst, "$t0");
        }
    }

    private void genMULT() {
        if (!Optimizer.isOptimise) {
            load(val1, "$t0");
            load(val2, "$t1");
            target.append("mult $t0, $t1\n");
            target.append("mflo $t0\n");
            store(dst, "$t0");
        } else {
            String reg1 = load(val1, "$t0");
            String reg3 = regPool.allocTReg(dst, index);
            if (mg.isNumber(val2)) {
                int power = getPowerOf2(Integer.parseInt(val2));
                if (power == 0) {
                    String reg2 = load(val2, "$t1");
                    target.append(String.format("mult %s, %s\n", reg1, reg2));
                    target.append(String.format("mflo %s\n", reg3));
                } else {
                    target.append(String.format("sll %s, %s, %d\n", reg3, reg1, Math.abs(power)));
                    if (power < 0) {
                        target.append(String.format("neg %s, %s\n", reg3, reg3));
                    }
                }
            } else {
                String reg2 = load(val2, "$t1");
                target.append(String.format("mult %s, %s\n", reg1, reg2));
                target.append(String.format("mflo %s\n", reg3));
            }
            if (reg3.equals("$t0")) {
                store(dst, "$t0");
            }
        }
    }

    private void genDIV() {
        if (!Optimizer.isOptimise) {
            load(val1, "$t0");
            load(val2, "$t1");
            target.append("div $t0, $t1\n");
            target.append("mflo $t0\n");
            store(dst, "$t0");
        } else {
            String reg1 = load(val1, "$t0");
            String reg3 = regPool.allocTReg(dst, index);
            if (mg.isNumber(val2)) {
                int power = getPowerOf2(Integer.parseInt(val2));
                if (power == 0) {
                    String reg2 = load(val2, "$t1");
                    target.append(String.format("div %s, %s\n", reg1, reg2));
                    target.append(String.format("mflo %s\n", reg3));
                } else {
                    String label1 = mg.genLabel();
                    String label2 = mg.genLabel();
                    target.append(String.format("bltz %s, %s\n", reg1, label1));  // $t0 正数
                    target.append(String.format("srl %s, %s, %d\n", reg3, reg1, Math.abs(power)));
                    if (power < 0) {
                        target.append(String.format("neg %s, %s\n", reg3, reg3));
                    }
                    target.append(String.format("j %s\n", label2));
                    target.append(String.format("%s:\n", label1));  // $t0 负数
                    target.append(String.format("neg $t0, %s\n", reg1));
                    target.append(String.format("srl %s, $t0, %d\n", reg3, Math.abs(power)));
                    if (power > 0) {
                        target.append(String.format("neg %s, %s\n", reg3, reg3));
                    }
                    target.append(String.format("%s:\n", label2));
                }
            } else {
                String reg2 = load(val2, "$t1");
                target.append(String.format("div %s, %s\n", reg1, reg2));
                target.append(String.format("mflo %s\n", reg3));
            }
            if (reg3.equals("$t0")) {
                store(dst, "$t0");
            }
        }
    }

    private void genMOD() {
        if (!Optimizer.isOptimise) {
            load(val1, "$t0");
            load(val2, "$t1");
            target.append("div $t0, $t1\n");
            target.append("mfhi $t0\n");
            store(dst, "$t0");
        } else {
            String reg1 = load(val1, "$t0");
            String reg3 = regPool.allocTReg(dst, index);
            if (mg.isNumber(val2)) {
                int power = getPowerOf2(Integer.parseInt(val2));
                if (power == 0) {
                    String reg2 = load(val2, "$t1");
                    target.append(String.format("div %s, %s\n", reg1, reg2));
                    target.append(String.format("mfhi %s\n", reg3));
                } else {
                    String label1 = mg.genLabel();
                    String label2 = mg.genLabel();
                    target.append(String.format("bltz %s, %s\n", reg1, label1)); // $t0 正数
                    target.append(String.format("andi %s, %s, %d\n", reg3, reg1, (int)(Math.pow(2, Math.abs(power))) - 1));
                    target.append(String.format("j %s\n", label2));
                    target.append(String.format("%s:\n", label1));  // $t0 负数
                    target.append(String.format("neg $t0, %s\n", reg1));
                    target.append(String.format("andi $t0, $t0, %d\n", (int)(Math.pow(2, Math.abs(power))) - 1));
                    target.append(String.format("neg %s, $t0\n", reg3));
                    target.append(String.format("%s:\n", label2));
                }
            } else {
                String reg2 = load(val2, "$t1");
                target.append(String.format("div %s, %s\n", reg1, reg2));
                target.append(String.format("mfhi %s\n", reg3));
            }
            if (reg3.equals("$t0")) {
                store(dst, "$t0");
            }
        }
    }

    private void genNOT() {
        String reg1 = load(val1, "$t0");
        String reg3 = regPool.allocTReg(dst, index);
        target.append(String.format("seq %s, %s, $0\n", reg3, reg1));
        if (reg3.equals("$t0")) {
            store(dst, "$t0");
        }
    }

    private void genSLT() {
        String reg1 = load(val1, "$t0");
        String reg2 = load(val2, "$t1");
        String reg3 = regPool.allocTReg(dst, index);
        target.append(String.format("slt %s, %s, %s\n", reg3, reg1, reg2));
        if (reg3.equals("$t0")) {
            store(dst, "$t0");
        }
    }

    private void genSLE() {
        String reg1 = load(val1, "$t0");
        String reg2 = load(val2, "$t1");
        String reg3 = regPool.allocTReg(dst, index);
        target.append(String.format("sgt %s, %s, %s\n", reg3, reg1, reg2));
        target.append(String.format("seq %s, %s, $0\n", reg3, reg3));
        if (reg3.equals("$t0")) {
            store(dst, "$t0");
        }
    }

    private void genSGT() {
        String reg1 = load(val1, "$t0");
        String reg2 = load(val2, "$t1");
        String reg3 = regPool.allocTReg(dst, index);
        target.append(String.format("sgt %s, %s, %s\n", reg3, reg1, reg2));
        if (reg3.equals("$t0")) {
            store(dst, "$t0");
        }
    }

    private void genSGE() {
        String reg1 = load(val1, "$t0");
        String reg2 = load(val2, "$t1");
        String reg3 = regPool.allocTReg(dst, index);
        target.append(String.format("slt %s, %s, %s\n", reg3, reg1, reg2));
        target.append(String.format("seq %s, %s, $0\n", reg3, reg3));
        if (reg3.equals("$t0")) {
            store(dst, "$t0");
        }
    }

    private void genSEQ() {
        String reg1 = load(val1, "$t0");
        String reg2 = load(val2, "$t1");
        String reg3 = regPool.allocTReg(dst, index);
        target.append(String.format("seq %s, %s, %s\n", reg3, reg1, reg2));
        if (reg3.equals("$t0")) {
            store(dst, "$t0");
        }
    }

    private void genSNE() {
        String reg1 = load(val1, "$t0");
        String reg2 = load(val2, "$t1");
        String reg3 = regPool.allocTReg(dst, index);
        target.append(String.format("sne %s, %s, %s\n", reg3, reg1, reg2));
        if (reg3.equals("$t0")) {
            store(dst, "$t0");
        }
    }

    private void genBEQ() {
        String reg1 = load(val1, "$t0");
        if (mg.isNumber(val2) && Integer.parseInt(val2) == 0) {
            target.append(String.format("beqz %s, %s\n", reg1, dst));
        } else {
            String reg2 = load(val2, "$t1");
            target.append(String.format("beq %s, %s, %s\n", reg1, reg2, dst));
        }
    }

    private void genBNE() {
        String reg1 = load(val1, "$t0");
        if (mg.isNumber(val2) && Integer.parseInt(val2) == 0) {
            target.append(String.format("bnez %s, %s\n", reg1, dst));
        } else {
            String reg2 = load(val2, "$t1");
            target.append(String.format("bne %s, %s, %s\n", reg1, reg2, dst));
        }
    }

    private String load(String name, String reg) { // 将name中的值取出放到寄存器reg中
        if (Optimizer.isOptimise && name.startsWith("T_")) { // 临时变量
            String t = regPool.getTReg(name);
            if (!t.equals("$t0")) {
                return t;
            }
        }
        if (mg.isNumber(name)) {
            target.append(String.format("li %s, %d\n", reg,  Integer.parseInt(name)));
        } else {
            Symbol symbol = table.searchGlobalBlock(name);
            if (symbol != null) {  // global value
                int offset = symbol.getOffset_gp();
                target.append(String.format("lw %s, %d($gp)\n", reg, offset));
            } else {
                symbol = table.searchFuncBlock(funcName, name);
                int offset = symbol.getOffset_sp();
                target.append(String.format("lw %s, %d($sp)\n", reg, offset));
            }
        }
        return reg;
    }

    private void store(String name, String reg) {  // 存储寄存器reg中的值
        Symbol symbol = table.searchGlobalBlock(name);
        if (symbol != null) {  // global value
            int offset = symbol.getOffset_gp();
            target.append(String.format("sw %s, %d($gp)\n", reg, offset));
        } else {
            symbol = table.searchFuncBlock(funcName, name);
            int offset = symbol.getOffset_sp();
            target.append(String.format("sw %s, %d($sp)\n", reg, offset));
        }
    }

    private int getPowerOf2(int value) {
        int v = value;
        int power = 0;
        while (v % 2 == 0) {
            v = v / 2;
            power++;
        }
        if (v == 1) {
            return power;
        } else if (v == -1) {
            return -power;
        } else {
            return 0;
        }
    }

    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        sb.append(opType.toString());
        sb.append(", ");
        if (val1 != null) {
            sb.append(val1);
        }
        sb.append(", ");
        if (val2 != null) {
            sb.append(val2);
        }
        sb.append(", ");
        if (dst != null) {
            sb.append(dst);
        }
        return sb.toString();
    }
}

